//pointer-events:none 对应标签元素的内容不可修改

//前端导出功能代码
//浏览器默认行为支持图片，txt文本,pdf等等预览 不支持压缩包，word预览
// 第一种方法：
// import { token } from 'utils'
// import qs from "qs"
// let _parmes = {
//   id: "skdjjf",
//   age: 20,
//   name: "xiaohua",
//   access_token:token()
// }
// VUE_APP_BASE_API是开发或生产环境中的变量也就是前缀(window.location.href链接结果如果是图片，txt文本,pdf会直接预览，不会下载)
// window.location.href = `${process.env.VUE_APP_BASE_API}/das/asset/export?${qs.stringify(_parmes)}`

// 第二种方法：downloadExportExcel是接口名字
// export function downloadExportExcel(params) {
// 	return request({
// 		url: '/das/archivesTransfer/archivesTransfer/exportStasticQuery',
// 		method: 'get',
// 		responseType: 'blob', 推荐加上
// 		params
// 	});
// }
// downloadExportExcel({
//   接口传递的参数
//   state: this.formSearch.status,
//   fondsCode: this.formSearch.allNum,
//   storageTime: this.formSearch.timeList,
//   year: this.formSearch.year,
// }).then(res => { request的参数中不加responseType: 'blob' res就是个二进制的文件流，必须执行res=new Blob(res)才能执行URL.createObjectURL(res)  加了responseType: 'blob'res就是一个blob对象可以直接执行URL.createObjectURL(res)
// let blobURL=window.URL.createObjectURL(res)
// let fileName = "移交信息统计列表.xlsx";  
// const elink = document.createElement("a");
// elink.download=fileName  //必须要设置下载名字，fileName让后端返给你  不加download下载，遇到图片，text,pdf会直接预览
// elink.href=blobURL
// elink.target="_blank"
// elink.style.display = "none";
// document.body.appendChild(elink);
// elink.click();
// window.URL.revokeObjectURL(blobURL);
// document.body.removeChild(elink);
//   };



//element中的样式穿透
{/* <style scoped lang='scss'>
//只能修改当.vue文件中element组件的样式
// vue2写法
  ::v-deep .el-table {
    thead tr th{
      border - right: 1px solid #dfe6ec;
      border-top: 1px solid #dfe6ec;
    }
    //vue3写法
    :deep(.el-card__body) {
      background: red;
    }
  }

</style> */}
// 修改全局样式
// <style>
//     .el-card__body {
//       background: red;
//     }
// </style>

//打印插件(vue-print-nb)
//vue2引入
// 1. 全局挂载
// import Print from 'vue-print-nb'
// Vue.use(Print)

// 2. 自定义指令
// import print from 'vue-print-nb'
// directives: {
//   print
// }
//vue3引入
// 1. 全局挂载
// import { createApp } from 'vue'
// import App from './App.vue'
// import print from 'vue3-print-nb'
// const app = createApp(App)
// app.use(print)
// app.mount('#app')

// 2. 自定义指令
// import print from 'vue3-print-nb'

// directives: {
//   print
// }

//在template中(id不能换成其他选择器会报错)
{/* <div id="xxx">
打印区域
<div class="showHide">页面不显示打印的时候显示</div>
</div>
// 第一种方式
<el-button type="primary" style="cursor: pointer;" v-print="'#xxx'">打印</el-button> */}
//第二种方式
{/* <el-button type="primary" style="cursor: pointer;" v-print="printObj">打印</el-button>  */ }
//在vue中定义变量(vue2中定义在data中，vue3中在setup中)
// printObj = {
//   id: 'xxx',
//   popTitle: '打印', // 打印配置页上方标题
//   extraHead: '', //最上方的头部文字，附加在head标签上的额外标签,使用逗号分隔
//   preview: '', // 是否启动预览模式，默认是false（开启预览模式，可以先预览后打印）
//   previewTitle: '', // 打印预览的标题（开启预览模式后出现）,
//   previewPrintBtnLabel: '', // 打印预览的标题的下方按钮文本，点击可进入打印（开启预览模式后出现）
//   zIndex: '', // 预览的窗口的z-index，默认是 20002（此值要高一些，这涉及到预览模式是否显示在最上面）   
//   previewBeforeOpenCallback() { }, //预览窗口打开之前的callback（开启预览模式调用）
//   previewOpenCallback() { }, // 预览窗口打开之后的callback（开启预览模式调用）
//   beforeOpenCallback() { }, // 开启打印前的回调事件
//   openCallback() { }, // 调用打印之后的回调事件
//   closeCallback() { }, //关闭打印的回调事件（无法确定点击的是确认还是取消）
//   url: '',//非局部打印有效，打印指定的URL，确保同源策略相同
//   standard: '',//非局部打印有效，异步加载打印指定的URL，确保同源策略相同
//   extraCss: '',//预览有效，预览窗口的z-index，默认是20002，最好比默认值更高
// }
//在css中
{/* <style scoped lang="scss">
  .showHide{ //页面不显示
    display: none;
  }
</style> */}

//全局打印样式
{/* <style>
  @media print {
  .showHide{ //打印的时候显示
    display: block!important;
  }
  @page{//去除页眉页脚
    size:landscape!important 横向打(适用于table列表打印)
    size:portrait!important 纵向打印
    size:auto;
    margin:0mm auto
  }
  html{
    height:auto;
  }
  body{ 
    在实际页面中高度不够部分内容隐藏了，这时候需要将对应块的内容为隐藏滚动的内容显示为全显示，
    打印时隐藏的内容并不会打印，只会打印到页面直接展示出来的内容
		height:auto;  
  }	
}
</style> */}

//element - ui下拉选框多选超出解决办法(浏览器兼容问题(奇安信样式有问题))
// .el - select.el - tag: nth - child(1) {
//   max - width: 100px;
//   overflow: hidden;
//   overflow: hidden;
//   text - overflow: ellipsis;
//   -o - text - overflow: ellipsis;
//   white - space: nowrap;
// }

//Vue router-view key属性的作用（推荐用$route.fullPath，<router-view :key="$route.fullPath"></router-view>）

// 1.不设置router-view的key属性

// 由于Vue会复用相同组件，即/page/a => /page/b或者/page?id=a => /page?id=b这类链接跳转时，将不再执行created，mounted之类的钩子，这时候你需要在路由组件中，添加beforeRouteUpdate钩子来执行相关方法拉取数据

// 相关钩子加载顺序为：beforeRouteUpdate

// 2.设置router-view的key属性值为$route.path

// 从/page/a => /page/b，由于这两个路由的$route.path并不一样，所以组件被强制不复用，相关钩子加载顺序为beforeRouteUpdate => created => mounted

// 从/page?id=a => /page?id=b，由于这两个路由的$route.path一样，所以和没设置key属性一样，会复用组件，相关钩子加载顺序为：beforeRouteUpdate

// 3.设置router-view的key属性值为$route.fullPath

// 从/page/a => /page/a，由于这两个路由的$route.fullPath并不一样，所以组件被强制不复用，相关钩子加载顺序为beforeRouteUpdate => created => mounted

// 4.从/page?id=a => /page?id=b，由于这两个路由的$route.fullPath并不一样，所以组件被强制不复用，相关钩子加载顺序为beforeRouteUpdate => created => mounted

//只能输入数字
// @input="value=>temp.shortName=value.replace(/[^\d]/g,'')"
{/* <el-form-item
  label="项目简称"
  prop="shortName"
>
  <el-input
    v-model.trim="temp.shortName"
    :disabled="isDisabled"
    placeholder=""
    clearable
    @input="value=>temp.shortName=value.replace(/[^\d]/g,'')"
  />
</el-form-item> */}

//keep-alive的生命周期和组件的生命周期执行顺序
//进入没有缓存页面的初始化顺序：beforeRouteEnter=>beforeCreated=>created=>beforeMounte=>mounted=>beforeRouteEnter中next回调参数函数的调用
//第一次进入缓存页面的初始化顺序beforeRouteEnter=>beforeCreated=>created=>beforeMounte=>mounted=>beforeRouteEnter中next回调参数函数的调用=>activated
//不是第一次进入缓存页面的初始化顺序beforeRouteEnter=>beforeRouteEnter中next回调参数函数的调用=>activated
//从缓存的页面离开缓存页面beforeRouteLeave=>deactivated
//从不是缓存的页面离开beforeRouteLeave=>beforeDestroy=>destroyed

//vue中用keep-alive  a=>b  b返回a   a页面缓存，同时滚动条也缓存到固定位置  其他页面到a页面   a页面清除缓存，正常加载
//keep-alive 缓存页面不生效的三种原因
// 1.组件中的name属性值和路由中的name属性值不一致，导致不生效！


{/* <keep-alive :include="cachedViews">  cachedViews存储在store中
    <router-view :key="$route.fullPath" />
</keep-alive>

<div style="height:500px;overflow-y: scroll;" ref="asodi" @scroll="onssss">
  <div style="height:1000px"></div>
</div> 

data(){
  return {
    scrollTop:0
  }
}

methods:{
  onssss(){
    滚动条位置的值赋值给this.scrollTop
    this.scrollTop=this.$refs.asodi.scrollTop
  }
}

async created() {
  await this.getUserList()
  this.getList()
  this.projectlistsTemplate()
},
async activated() {
  // await this.getUserList()
  // this.getList()
  // this.projectlistsTemplate()
  由于keep-alive不会缓存滚动条位置所以需要给滚动条位置重新赋值
  this.$refs.asodi.scrollTop=this.scrollTop
},
缓存应该是你进去的时候页面为缓存状态true离开的时候也应该是true(https://juejin.cn/post/6844903840156745736#heading-2)
beforeRouteEnter(to, from, next) {
  next(vm => {
    if (!(vm.$store.state.tagsView.cachedViews.includes('ProjectmanagementItemlistpath'))) {
      vm.$store.state.tagsView.cachedViews.push('ProjectmanagementItemlistpath')
    }
  })
},
beforeRouteLeave(to, from, next) {
  if (to.path === '/codeManagement/result') {
    if (!(this.$store.state.tagsView.cachedViews.includes('ProjectmanagementItemlistpath'))) {
      this.$store.state.tagsView.cachedViews.push('ProjectmanagementItemlistpath')
    }
  } 
  else {
    if (this.$store.state.tagsView.cachedViews.includes('ProjectmanagementItemlistpath')) {
      const index = this.$store.state.tagsView.cachedViews.indexOf('ProjectmanagementItemlistpath')
      this.$store.state.tagsView.cachedViews.splice(index, 1)
    }
  }
  next()
}, */}


{
  keep-alive 的 include缓存的组件中子孙组件也能缓存吗？
 //结论
// keep-alive 的 include（或 exclude）只对 被 <keep-alive> 包裹的 直接子组件 起作用。
// 当一个被 include 命中的组件被缓存时，它内部的 普通子组件（即在模板里直接写 <child‑comp/> 的那层）会随父组件一起被保存在内存中，不需要再额外声明。
// 但 通过 <router-view>、<component :is="...">、v‑if/v‑show 动态切换的子组件，它们本身是 独立的 VNode 实例，如果想让这些子组件也被缓存，
// 需要 在父层再套一层 <keep-alive> 并在 include 中加入它们的 name，否则它们在切换时会被销毁并重新创建。

// 普通子组件（在父组件模板里直接写 <my-child/>）‍不需要单独列入 include，它们随父组件一起被缓存。
// 子路由（<router-view>）或动态组件 是 独立的根组件实例，它们的缓存与父组件的缓存是相互独立的。若想让这些子路由也保持缓存，需要在父层再包一层 <keep-alive>，并在 include 中加入子路由对应组件的 name。
// 2. 常见场景对比
// 场景	是否需要额外 include	说明
// 父组件 A 包含普通子组件 B、C（<b/> <c/>）	❌ 不需要	B、C 随 A 的实例一起被缓存。
// 父组件 A 中使用 <router-view/> 渲染子路由页面 D、E	✅ 需要在外层再包 <keep-alive> 并把 D、E 的 name 加入 include	<router-view> 会创建独立的路由组件实例，默认不受父层 keep-alive 影响。
// 父组件 A 中使用 <component :is="currentComp"/> 动态切换组件 F、G	✅ 同上，需要在外层再包 <keep-alive> 并把 F、G 的 name 加入 include	动态组件本质上是独立的根组件实例。
// 父组件 A 中使用 v-if 控制子组件 H 的显示/隐藏	❌（如果 H 仍在 A 的模板里）	v-if 会销毁/重新创建 H，但因为 H 属于 A 的实例，A 被缓存后再次激活时会恢复上一次的状态。如果想让 H 在切换时保持状态，建议改用 v-show 或把 H 单独包 <keep-alive>。
// 3. 实际代码示例
// <!-- 根层：只缓存页面级组件（通过 name） -->
// <keep-alive :include="cachedPages">
//   <router-view />
// </keep-alive>

// <!-- 某个页面组件（PageA）内部 -->
// <template>
//   <div>
//     <!-- 普通子组件，随 PageA 一起缓存 -->
//     <HeaderComp />
//     <ContentComp />

//     <!-- 子路由，需要单独缓存 -->
//     <keep-alive :include="['SubPage1','SubPage2']">
//       <router-view />
//     </keep-alive>

//     <!-- 动态组件，同样需要单独缓存 -->
//     <keep-alive :include="['WidgetA','WidgetB']">
//       <component :is="activeWidget" />
//     </keep-alive>
//   </div>
// </template>

// <script>
// export default {
//   name: 'PageA',          // 这里的 name 必须与根层 include 中的名称匹配
// }
// </script>
// HeaderComp、ContentComp 不需要额外声明，它们随 PageA 的实例一起被缓存。
// SubPage1/2（子路由）和 WidgetA/B（动态组件）因为是 独立根实例，必须在各自的层级再使用 <keep-alive> 并在 include 中列出它们的 name，才能实现缓存。
// 4. 小结
// **include 控制的是 被 <keep-alive> 包裹的 直接 组件实例。
// 普通子组件 自动随父组件一起缓存，无需额外配置。
// 子路由、动态组件、通过 v-if 切换的根组件 需要 在对应层级再套 <keep-alive> 并在 include 中列出它们的 name，才能实现缓存。
// 这样既能保证页面级的整体缓存，又能对需要单独保持状态的子页面或动态组件进行精细化控制。
}


//element上传组件 before-upload返回false自动调用before-remove on-remove钩子问题解决方法
//删除文件之前
//  beforeRemove(file, fileList) {
//   if (file && file.status==="success") {
//      ......
//   }
// },
// //删除文件
// handleRemove(file, fileList) {
//   if (file && file.status==="success") {
//     ......
//   }
// }

//前端上传的file文件转换成base64编码上传到服务器 element的上传组件需要用自定义上传base64编码(因为element ui默认上传组件是基于form-data文件流实现的)
// 自定义上传
//获取BASE64
// const changeBASE64 = file => {
//   return new Promise(resolve => {
//     let fileReader = new FileReader();
//     fileReader.readAsDataURL(file);//readAsDataURL 把file文件流转化成base64, 发起异步请求
//     fileReader.onload = ev => {//请求成功的回调
//       // encodeuricomponent什么时候使用：用于url作为参数传递的场景中使用,url当作参数传递的时候，当参数出现空格这样的特殊字段，后台只可以读取到空格前的内容，后面内容丢失，
//       // 造成数据读取失败，但是如果用encodeURIComponent()，则这些特殊字符进行转义，这样后台就可以成功读取了，所以encodeURIComponent()用于url作为参数传递的场景中使用
//       //decodeURIComponent() 函数可对 encodeURIComponent() 函数编码的 URI 进行解码。
//       resolve(ev.target.result);//ev.target.result就是base64编码字符串 它其实也可以作为url在浏览器中打开预览，也可以赋值给image中的src显示图片
//     };
//   });
// };
// async httpRequest(file) {
//   this.dataParmes.file = encodeURIComponent(await changeBASE64(file))
//   const res = await projectupload(this.dataParmes);//dataParmes是data中定义的变量
//   if (res?.code === 200) {
//     this.temp.path = res.data;
//     this.$message({
//       //message: res.message,
//       message: "上传成功",
//       type: "success"
//     });
//   }
// }

// 大文件上传详细示例(https://zhuanlan.zhihu.com/p/467714393)
// 上传文件成功之后纯前端实现预览(把file文件流转化成base64，赋值给image的src显示图片)
//前端基于Spark-MD5.js(需要引入这个js文件),file文件流根据文件的内容(hash值是根据文件内容实现的，内容改变，hash就改变，hash值是唯一,调用上传接口的时候吧fileName传给后端)生成后端需要的文件名
const hashChange = (file) => {
  return new Promise(resolve => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(file);
    reader.onload = function (e) {
      const buffer = e.target.result
      let spark = new SparkMD5.ArrayBuffer()
      spark.append(buffer)
      let hash = spark.end()
      let index = file.name.lastIndexOf(".")
      let suffix = file.name.slice(index)
      resolve({
        buffer,
        suffix,
        hash,
        fileName: `${hash}${suffix}`
      })
    }
  })
}
// let {
//   filename
// } = await hashChange(_file);
// let formData = new FormData();
// formData.append('file', _file);
// formData.append('filename', filename);//把生成的filename传给后端，后端把filename作为接受文件的新名字
// instance.post('/upload_single_name', formData).then(data => {
//   if (+data.code === 0) {
//     alert(`文件已经上传成功~~,您可以基于 ${data.servicePath} 访问这个资源~~`);
//     return;
//   }
//   return Promise.reject(data.codeText);
// }).catch(reason => {
//   alert('文件上传失败，请您稍后再试~~');
// })


//无限滚动的使用(用v-mome和.passive修饰符做性能优化)
//1. 不用ui库，正常写法
// data(){
//   return {
//     lists:[]，
//     total:0,
//     page:1 
//   }
// }
// <div style="overflow-y:auto;overflow-x:hidden;height:332px" @scroll.passive="handleScroll" ref="handleScroll">
//     <div v-for="(item,index) in lists" v-mome="[item.id]">
//        ....
//     </div>
// </div>
// async paramsListsType() {
//   const res = await paramsListsType();
//   if (res?.code === 200) {
//     this.lists=[this.lists,...res.data]
//     this.total=res.total
//   } 
// }
handleScroll(e) {
  // const { scrollHeight, scrollTop, clientHeight } = e.srcElement; //这里e.srcElement就等于this.$refs.handleScroll
  // console.log(scrollHeight, scrollTop, clientHeight, 620000);
  // if (scrollTop + clientHeight>=scrollHeight) {
  //   console.log("触底啦！");
  //   调用后台接口向lists中添加数据
  //     if(this.lists.length<this.total){
  //       this.page++
  //       this.paramsListsType()
  //     }
  // }
  ///requestAnimationFrame的用法(requestAnimationFrame这个api主要是用来做动画的)
  let timer=requestAnimationFrame(function (){
    const { scrollHeight, scrollTop, clientHeight } = e.srcElement;
    if (scrollTop + clientHeight>=scrollHeight) {
        if(this.lists.length<this.total){
          this.page++
          this.paramsListsType()
          cancelAnimationFrame(timer)
        }
    }
  })
}

// 2.使用element ui table列表的无限滚动
{/* <el-table :data="tableData" style="width: 50%" height="396" ref="tablesssData">
  <el-table-column prop="date" label="日期"></el-table-column>
</el-table>
data(){
  return {
    tableData:[],
    totalCount:0,
    listsParmes:{
      page:1,
      size:10
    }
  }
}
methods:{
  async paramsListsType() {
      const res = await paramsListsType(this.listsParmes);
      if (res?.code === 200) {
        this.tableData=[this.tableData,...res.data]
        this.totalCount=res.total
      } 
    }
},
beforeDestroy(){
  this.bodyWrapper.removeEventListener("scroll",this.scrollFn)
},
mounted() {
  this.paramsListsType()
  const bodyWrapper = this.$refs.tablesssData.querySelector(".el-table__body-wrapper");
  this.bodyWrapper=bodyWrapper
  bodyWrapper.style["overflow-y"] = "auto";
  this.scrollFn=()=>{
    if (bodyWrapper.scrollTop + bodyWrapper.clientHeight >= bodyWrapper.scrollHeight) {
      if (this.tableData.length < this.totalCount) {
        this.listsParmes.page++
        this.paramsListsType()
      }else{
        this.$message.warning("到底啦")
      }
    }
  }
  bodyWrapper.addEventListener("scroll", this.scrollFn);
} */}

// 或者用IntersectionObserver(图片懒加载https://blog.csdn.net/weixin_43288600/article/details/132239842,https://www.jianshu.com/p/1a0bf99d4c1f,
无限滚动https://blog.csdn.net/kaka_buka/article/details/131666708和https://juejin.cn/post/7250080519068565565)
<template>
  <div style="height: 300px; overflow-y: auto" id="refUl">
    <ul>
      <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
    <div ref="observerElement"></div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";

const items = ref([
  { id: 4, name: "Item 4" },
  { id: 5, name: "Item 5" },
  { id: 6, name: "Item 6" },
  { id: 4, name: "Item 4" },
  { id: 5, name: "Item 5" },
  { id: 6, name: "Item 6" },
  { id: 4, name: "Item 4" },
  { id: 5, name: "Item 5" },
  { id: 6, name: "Item 6" },
  { id: 4, name: "Item 4" },
  { id: 5, name: "Item 5" },
  { id: 6, name: "Item 6" },
  { id: 4, name: "Item 4" },
  { id: 5, name: "Item 5" },
  { id: 6, name: "Item 6" },
  { id: 4, name: "Item 4" },
  { id: 5, name: "Item 5" },
  { id: 6, name: "Item 6" },
  { id: 4, name: "Item 4" },
  { id: 5, name: "Item 5" },
  { id: 6, name: "Item 6" },
  { id: 4, name: "Item 4" },
  { id: 5, name: "Item 5" },
  { id: 6, name: "Item 6" },
]);
const observerElement = ref(null);
onMounted(() => {
  const observer = new IntersectionObserver(
    (entries, observer) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          // 当触底时加载更多元素
          console.log(123456);
          loadMoreItems();
        }
      });
    },
    {
      root: document.getElementById("refUl"),
    }
  );

  observer.observe(observerElement.value);

  // 模拟异步加载更多元素
  const loadMoreItems = async () => {
    // 发起异步请求获取更多数据
    const newItems = await fetchMoreItems();

    // 更新items数组
    items.value = items.value.concat(newItems);
  };

  // 模拟异步请求获取更多数据
  const fetchMoreItems = async () => {
    // 发起异步请求，获取更多数据
    // 返回一个Promise，包含新的元素数组
    // 这里可以根据实际需求来实现数据获取逻辑
    return new Promise((resolve) => {
      setTimeout(() => {
        const newItems = [
          { id: 4, name: "Item 4" },
          { id: 5, name: "Item 5" },
          { id: 6, name: "Item 6" },
          { id: 4, name: "Item 4" },
          { id: 5, name: "Item 5" },
          { id: 6, name: "Item 6" },
          { id: 4, name: "Item 4" },
          { id: 5, name: "Item 5" },
          { id: 6, name: "Item 6" },
          { id: 4, name: "Item 4" },
          { id: 5, name: "Item 5" },
          { id: 6, name: "Item 6" },
        ];
        resolve(newItems);
      }, 1000);
    });
  };
});
</script>
{/* IntersectionObserver无线滚动 */}


//虚拟滚动的使用(对于scroll,resize,mousemove等事件，短时间可能被频繁触发，影响性能。若回调函数中涉及到元素重绘、DOM操作等耗时工作，可能导致在下一次scroll触发前回调未完成，造成掉帧)
// 1.原生for循环的使用
{/* <div
  class="viewPort"
  ref="viewPortRef"
  @scroll="onscroll"
>
  <div class="scroll" ref="scrollRef"></div>
  <div class="list" ref="listRef">
    <!-- :style="{ height: `${obj.height}px` }" -->
    <div v-for="item in showLists" :key="item.num" class="row">{{ item.num }}</div>
  </div>
</div> 
// props:{
  start:{ //可视区域开始的下角标
   type:Number,
   default:0
  },
  height:{ //可视区域每一行的高度
   type:Number,
   default:20
  },
  viewCount:{ //可视区域总共多少行
   type:Number,
   default:30
  },
},
 created(){
  this.lists = new Array(200).fill(null).map((item, index) => ({
   num: index + 1
 }));
},
 mounted() {
  this.$refs.viewPortRef.style.height = this.height * this.viewCount + "px";
  this.$refs.scrollRef.style.height = this.height * this.lists.length + "px";
},
 computed: {
  showLists() {
    return this.lists.slice(this.start, this.start + this.viewCount);
  }
},
 methods:{
  onscroll() {
    用requestAnimationFrame优化下这个滚动内容能解决白屏吗
    const timer= requestAnimationFrame(function(){
      const scrollTop = this.$refs.viewPortRef.scrollTop;
      this.$parent.start = Math.round(scrollTop / this.height);
      this.$refs.listRef.style.transform = `translateY(${scrollTop}px)`;
      cancelAnimationFrame(timer)
    })
  },
  framescroll(){
    const scrollTop = this.$refs.viewPortRef.scrollTop;
    this.$parent.start = Math.round(scrollTop / this.height);
    this.$refs.listRef.style.transform = `translateY(${scrollTop}px)`;
  }
}

// .viewPort {
//   width: 300px;
//   // height:600px
//   position: fixed;
//   left: 20%;
//   top: 50%;
//   transform: translate(-50%, -50%);
//   background: yellowgreen;
//   overflow-y: scroll;
// }
// .list {
//   position: absolute;
//   left: 0;
//   top: 0;
// }
// .scroll {
//   // height: 3000px;
// }
// .row {
//   height: 20px;
// }
// 2.element ui中table列表中使用
{/* <el-table :data="computedDta" style="width: 100%" v-scrollingVirtual="scrollObj" height="396">
  <el-table-column prop="date" label="日期"></el-table-column>
</el-table>
data(){
  return {
    scrollObj:{
      lieRowHeight:36,
      lierows:10,
      start:0,
      tableData:[]
    }
  }
}
created(){
  const lists=new Array(20000).fill(null).map((item,index)=>({date:index+1})) //lists从后端拿所有数据
  this.scrollObj.tableData=Object.freeze(lists)
},
computed:{
  computedDta(){
      return this.scrollObj.tableData.slice(this.scrollObj.start,this.scrollObj.start+this.scrollObj.lierows)
  }
},
//组件中指令的写法
directives:{
  scrollingVirtual:{
      bind(el, binding, vnode) {
        //vnode.context是指令所在组件的this
        const bodyWrapper = el.querySelector(".el-table__body-wrapper");
        el.bodyWrapper=bodyWrapper
        const tablebody=el.querySelector(".el-table__body");
        const scrllHdom=document.createElement("div")
        bodyWrapper.style['overflow-y']="auto"
        scrllHdom.style.height=binding.value.tableData.length*binding.value.lieRowHeight+"px"
        bodyWrapper.append(scrllHdom)
        tablebody.style.position="absolute";
        el.load=()=>{
            const scrollTop=bodyWrapper.scrollTop
            binding.value.start=Math.round(scrollTop / binding.value.lieRowHeight)
            tablebody.style.transform = `translateY(${scrollTop}px)`;
        }
        bodyWrapper.addEventListener("scroll", el.load);
      },
      unbind(el){
        el.bodyWrapper.removeEventListener("scroll",el.load)
      }
  }
}
main.js中全局自定义指令的用法
import Vue from 'vue'
Vue.directive('scrollingVirtual', {
  bind(el, binding, vnode) {
    //vnode.context是指令所在组件的this
    const bodyWrapper = el.querySelector(".el-table__body-wrapper");
    el.bodyWrapper=bodyWrapper
    const tablebody=el.querySelector(".el-table__body");
    const scrllHdom=document.createElement("div")
    bodyWrapper.style['overflow-y']="auto"
    scrllHdom.style.height=binding.value.tableData.length*binding.value.lieRowHeight+"px"
    bodyWrapper.append(scrllHdom)
    tablebody.style.position="absolute";
    el.load=()=>{
        const scrollTop=bodyWrapper.scrollTop
        binding.value.start=Math.round(scrollTop / binding.value.lieRowHeight)
        tablebody.style.transform = `translateY(${scrollTop}px)`;
    }
    bodyWrapper.addEventListener("scroll", el.load);
  },
  unbind(el){
    el.bodyWrapper.removeEventListener("scroll",el.load)
  }
}) */}
//在vue3中使用
// const scrollObj=reactive({
//   lieRowHeight:36,//每一行的高度
//   lierows:10,//可视区域显示多少条数据
//   start:0,//可视区域开始的下角标
//   tableData:[]//所有数据
// })
// const lists=new Array(20000).fill(null).map((item,index)=>({date:index+1}))
// scrollObj.tableData=markRaw(lists)
// const computedDta=computed(()=>{
//   return scrollObj.tableData.slice(scrollObj.start,scrollObj.start+scrollObj.lierows)
// })
// //全局注册(vue3全局注册指令的名字开头不要加v)
// app.directive("scrollingVirtual",{
//   mounted(el, binding) {
//     const bodyWrapper = el.querySelector(".el-table__body-wrapper");
//     el.bodyWrapper=bodyWrapper
//     const tablebody=el.querySelector(".el-scrollbar");
//     const scrllHdom=document.createElement("div")
//     bodyWrapper.style['overflow-y']="auto"
//     scrllHdom.style.height=binding.value.tableData.length*binding.value.lieRowHeight+"px"
//     bodyWrapper.append(scrllHdom)
//     tablebody.style.position="absolute";
//     el.load=()=>{
//         const scrollTop=bodyWrapper.scrollTop
//         binding.value.start=Math.round(scrollTop / binding.value.lieRowHeight)
//         console.log(binding.value.start,2222222222)
//         tablebody.style.transform = `translateY(${scrollTop}px)`;
//     }
//     bodyWrapper.addEventListener("scroll", el.load);
//   },
//   unmounted(el) {
//     el.bodyWrapper.removeEventListener("scroll",el.load)
//   }
// })

//vue中经常用到dom监听，dom操作相关的逻辑复用 推荐使用自定义指令

//组件销毁时的资源释放(Vue组件销毁时，会自动解绑它的全部指令及事件监听器，但是仅限于组件本身的事件)
//为避免内存泄露，不论是手动销毁还是自动销毁，都需要在生命周期钩子中处理组件使用的资源。如在beforeDestroy钩子中清除对DOM的引用、清除定时器、解绑event bus的事件($off)、清除事件监听(addEventListener注册的事件监听器)

//Vue中报“Duplicate keys detected: ‘xxx‘. This may cause an update error.”的解决办法
// 这一般是因为v-for循环中的key造成，将循环的次数:key="index"加上即可。

// <div v-for="(list ,index) in dataList" :key="index">{{list.title}}</div>

// 如果，同时存在多个v-for，key就会被覆盖，此时，只要保证key不一样即可，如下所示：

// <div v-for="(list ,index) in dataList" :key="index+'1'">{{list.title}}</div>

// <div v-for="(list ,index) in dataList" :key="index+'2'">{{list.title}}</div>


// element ui中（比如el-select、el-button、el-input等组件）中的 change、click、focus、事件是经过源码封装过的所以用的时候不用加.native,其他事件在用的时候需要加.native
// <el-button
// class="filter-item"
// type="primary"
// icon="el-icon-search"
// @mouseenter.native="shdt(2)"
// @mousemove.native="shdt(3)"
// @mouseleave.native="shdt(4)"
// @click="shdt(0)"
// >查询</el-button> //click不需要加.native    mouseenter，mousemove，mouseleave需要加.native

// vue中.capture事件捕获修饰符的用法(事件冒泡和捕获的使用场景要么都是click,要么都是mouseleave，。。。)
{/* <div id="obj1" v-on:click.capture="doc">
obj1                   点击obj1结果是obj1
<div id="obj2" v-on:click.capture="doc">
  obj2                 点击obj2结果是obj1 obj2
  <div id="obj3" v-on:click="doc">
    obj3               点击obj3结果是obj1 obj2 obj3
    <div id="obj4" v-on:click="doc">
      obj4
      <!--。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。点击obj4的时候，弹出的顺序为：obj1、obj2、obj4、obj3；
      由于1，2有修饰符，故而先触发事件，然后就是4本身触发，最后冒泡事件。
      -->
    </div>
  </div>
</div>
</div> */}

// vue中.passive事件捕获修饰符的用法(.passive 修饰符一般用于触摸事件的监听器，可以用来改善移动端设备的滚屏性能)
// wheel：滚动鼠标滚轮后，首先去触发 wheel滚动事件函数，函数执行完毕后，再去执行默认行为(滚动条移动)==>这个环节就是浪费时间了，如果wheel滚动事件函数执行时间较长，页面就会卡顿。
// 解决办法就是：passive   滚动鼠标滚轮后事件的默认行为立即执行，不用等待事件回调函数执行完毕

// keyup和keydown的使用(适用于input和textarea元素，其他元素用法参考https://segmentfault.com/a/1190000041568137?sort=votes这个连接)


//Vue2.x-$on,$once,$off以及$emit的使用
// created() {
//   this.$on('ChildClickFn', arg => {
//       console.log('我是用$on监听事件并触发的，子组件的clickFn被执行了,参数是:', arg);
//   });
//   this.$once('ChildClickFn', arg => {
//       console.log('我是用$once监听事件并触发的,参数是:', arg);
//   });
// }
// $once
// 　　使用：vm.$once('事件名称',callback)

// 　　说明：监听当前实例(vm)中的自定义事件，事件可以由$emit定义,但是只会触发一次，触发后即解除//监听事件被触发




// vue3+TS的应用
//为 provide / inject 标注类型
// type.ts
// import { InjectionKey, Ref } from 'vue'
 
// export interface User {
//   name: string
//   age: number
// }
 
// export type SetUser = (newUser: User) => void
 
// // 对象的InjectionKey
// export const userKey: InjectionKey<Ref<User>> = Symbol('')
 
// // 函数的的InjectionKey
// export const setUserKey: InjectionKey<SetUser> = Symbol('')

// //引用上面的type.ts
// import { userKey, setUserKey } from './type'
 
// const user = ref({ name: 'Karen', age: 20 })
// provide(userKey, user)
 
// const setUser = ({ name, age }: { name: string; age: number }) => {
//   user.value.name = name
//   user.value.age = age
// }
// provide(setUserKey, setUser)


// vue3中用createRenderer渲染Canvas平台(https://aihongxin.com/9983.html)
// requestAnimationFrame 会把每一帧中的所有DOM操作集中起来，在一次重绘或回流中就完成，并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率，一般来说，这个频率为每秒60帧
// 由于requestAnimationFrame的功效只是一次性的，所以若想达到动画效果，则必须连续不断的调用requestAnimationFrame，就像我们使用setTimeout来实现动画所做的那样。
// 可以这么说，requestAnimationFrame就是一个性能优化版、专为动画量身打造的setTimeout，不同的是requestAnimationFrame不是自己指定回调函数运行的时间，而是跟着浏览器内建的刷新频率来执行回调，
// 这当然就能达到浏览器所能实现动画的最佳效果了
//vue返回顶部的组件BackTop
{/* <template>
  <div @click="backTopBtn(move)">
    <slot />
  </div>
</template>

<script>
export default {
  name: "BackTop",
  props: {
  	//每一帧移动的距离
    move: {
      type: Number,
      default: 300,
    },
  },
  methods: {
    backTopBtn(move) {
      cancelAnimationFrame?cancelAnimationFrame(this.timer):clearTimeout(this.timer); 
      const fn = () => {
        const scrollY = document.documentElement.scrollTop;
        if (scrollY > 0) {
          document.documentElement.scrollTop = scrollY - move;
          this.timer = requestAnimationFrame?requestAnimationFrame(fn):setTimeout(fn, 1000/60);
        } else {
          cancelAnimationFrame?cancelAnimationFrame(this.timer):clearTimeout(this.timer);
        }
      };
      this.timer = requestAnimationFrame?requestAnimationFrame(fn):setTimeout(fn, 1000/60);
    },
  },
};
</script>
<style>
</style> */}

// 使用requestAnimationFrame实现进度条
// <!DOCTYPE html>
// <html lang="en">
// <head>
//     <meta charset="UTF-8">
//     <meta name="viewport" content="width=device-width, initial-scale=1.0">
//     <title>Document</title>
// </head>
// <body>
//     <div style="width:0px;background:red">0%</div>
// </body>
// <script>
//     //requestAnimationFrame使用场景(https://zhuanlan.zhihu.com/p/656701597,https://wenku.baidu.com/view/adf894b888d63186bceb19e8b8f67c1cfad6eeea.html?_wkts_=1703900396064&bdQuery=requestAnimationFrame%E7%9A%84%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF&needWelcomeRecommand=1)
    // requestAnimationFrame使用(https://blog.csdn.net/handsomexiaominge/article/details/89057456,https://www.jianshu.com/p/2771cb695c81)
    const divDom=document.querySelector("div")
    divDom.addEventListener("click",function(){
        let timer=requestAnimationFrame(function fn(DOMHighResTimeStamp){
            console.log(DOMHighResTimeStamp,6300); //该参数与 performance.now() 的返回值相同，它表示 requestAnimationFrame() 开始执行回调函数的时刻
            if(parseInt(divDom.style.width)<100){
                divDom.style.width=parseInt(divDom.style.width)+5+"px"
                divDom.innerHTML=parseInt(divDom.style.width)+"%"
                cancelAnimationFrame(timer)
                timer=requestAnimationFrame(fn)
            }else{
                cancelAnimationFrame(timer)
            }
        })
        
        // let timer=setTimeout(function fn(){
        //     if(parseInt(divDom.style.width)<100){
        //         divDom.style.width=parseInt(divDom.style.width)+5+"px"
        //         divDom.innerHTML=parseInt(divDom.style.width)+"%"
        //         clearTimeout(timer)
        //         timer=setTimeout(fn,100)
        //     }else{
        //         clearTimeout(timer)
        //     }
        // },100)
    })
// </script>
// </html>
// requestAnimationFrame的使用

//vue3 effectScope和onScopeDispose的使用场景

// index.js中(这里用effectScope和onScopeDispose是为了在多个组件中复用同一个x,y的ref和事件监听,移除)
// function useMouse() {
//   const x = ref(0)
//   const y = ref(0)

//   function handler(e) {
//     x.value = e.x
//     y.value = e.y
//   }

//   window.addEventListener('mousemove', handler)

//   onScopeDispose(() => {
//     window.removeEventListener('mousemove', handler)
//   })

//   return { x, y }
// }

// function createSharedComposable(composable) {
//   let subscribers = 0
//   let state, scope

//   const dispose = () => {
//     if (scope && --subscribers <= 0) {
//       scope.stop()
//       state = scope = null
//     }
//   }

//   return (...args) => {
//     subscribers++
//     if (!state) {
//       scope = effectScope(true)
//       state = scope.run(() => composable(...args))
//     }
//     onScopeDispose(dispose)
//     return state
//   }
// }
// export const useSharedMouse = createSharedComposable(useMouse)
// index.js中(这里用effectScope和onScopeDispose是为了在多个组件中复用同一个x,y的ref和事件监听,移除)

// 在多个vue组件的setup中直接引入useSharedMouse并const {x, y} = useSharedMouse(),在组件模板中显示x,y


// 性能优化参考(https://www.bilibili.com/video/BV1TP4y1D7cK/?spm_id_from=333.788&vd_source=009ec7b09b6a18588257bad29cd0d0cc)


// 多次修改dom属性合并写法如下(性能比一个一个操作dom要好)
// // 不推荐
// const element = document.getElementById('box');
// element.style.width = '100px';
// element.style.height = '100px';
// element.style.margin = '10px';

// // 推荐1
// const element = document.getElementById('box');
// element.classList.add('box-style');

// // CSS
// .box-style {
// width: 100px;
// height: 100px;
// margin: 10px;
// }
  //推荐2
// oDiv.style.cssText = ' width:100px; height:100px; margin: 10px;'//样式必须写全，因为这种写法时样式覆盖


//使用 will-change 提示
// .animated-element {
//   will-change: transform, opacity;
// }
//注意：不要过度使用 will-change，只在真正需要的元素上使用。


// 前端性能优化最佳实践(https://segmentfault.com/a/1190000041753539,https://juejin.cn/post/7188894691356573754,https://blog.csdn.net/weixin_30839881/article/details/94891129)
// css的性能优化
// 1.优先使用transform、opacity通过GPU加速 ，其属性的改变不会发生重排和重绘。如位移操作的，可以使用translate 来实现，渐隐渐现效果可以使用 opacity 属性来实现
// 2.少用box-shadows与gradients他们是性能杀手
// 3.对于动画元素，尽量使用 fixed、absolute 定位方式(脱离文档流)，避免影响到其他节点重排
// 4.css尽量不要嵌套的代码因为css查早规则是从右向左，最好用class选择器，其他选择器最好不用(id选择器必须唯一不通用，可能会有意外的错误)

// js的性能优化
// 1.创建多个dom添加到某个标签中推荐使用createDocumentFragment
// 例如：
// // bad
// const div=document.querySelector("div") //已知的dom
// for(let i=0;i<10;i++){
//   div.append(document.createElement("p"))
// }

// //good
// const div=document.querySelector("div") //已知的dom
// const fragment = document.createDocumentFragment();
// for(let i=0;i<10;i++){
//   fragment.append(document.createElement("p"))
// }
// div.append(fragment)

// 2.如果某个样式是通过重排得到的，那么最好缓存结果。避免下一次用到的时候，浏览器又要重排
// 例如：
// // bad
// for(let i=0;i<10;i++){
//   div.offsetLeft+=10
//   div.offsetLeft+=10
// }
// // 从性能角度考虑，尽量不要把读操作和写操作，放在一个语句里面
// div.style.left = div.offsetLeft + 10 + "px";
// div.style.top = div.offsetTop + 10 + "px";

// // good
// var left = div.offsetLeft;
// var top  = div.offsetTop;
// for(let i=0;i<10;i++){
//   left+10
//   top+=10
// }
// el.style.cssText =`left:${left};top:${top};`

// 3.不要一条条地改变样式，而要通过改变class，或者csstext属性，一次性地改变样式(el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;')
// 4.避免使用定时器，而用requestAnimationFrame代替setTimeout实现动画
// 5.用webworker开启子线程处理耗时的计算任务通过postMessage和onmessage事件进行数据通信
// Web Worker使用的注意事项(https://www.bilibili.com/video/BV1AU4y1i76g/?spm_id_from=333.999.0.0&vd_source=009ec7b09b6a18588257bad29cd0d0cc)
// Web Worker 可以在很多场景下使用，特别是在需要处理大量数据或计算密集型任务时。以下是一些 Web Worker 的使用场景：
// 图像处理：Web Worker 可以用于处理图像操作，如旋转、裁剪、缩放、滤镜等。这可以提高图像处理的性能和响应速度。
// 数据处理：Web Worker 可以用于处理大量数据，如数据集的过滤、排序、归纳、转换等。这可以提高数据处理的效率和准确性。
// 计算密集型任务：Web Worker 可以用于处理计算密集型任务，如模拟、优化、预测、统计等。这可以提高计算任务的速度和精度。
// 实时通信：Web Worker 可以用于实现实时通信，如聊天室、游戏、视频会议等。这可以提高实时通信的稳定性和性能。
// 离线缓存：Web Worker 可以用于离线缓存，可以将常用的资源预先下载到客户端本地缓存中，并在无法访问互联网时使用缓存中的资源。这可以提高应用程序的可用性和响应速度，尤其是在移动设备上。
// 多线程处理：Web Worker 可以用于实现多线程处理，如并行计算、任务分发、负载均衡等。这可以提高系统的并发性和可伸缩性。
// 总之，Web Worker 可以用于任何需要在后台执行长时间运行的任务，以避免阻塞 UI 线程和提高应用程序的性能和响应速度
// 计算的运算时长 - 通信时长 > 50ms，推荐使用Web Worker 通信时长就是新建一个web worker时, 浏览器会加载对应的worker.js资源的时长
// 6.使用防抖和节流
// 7.dom操作使用事件委托
// 8.IntersectionObserver 触底加载和懒加载 比scroll性能好


// dataset的使用(https://www.cnblogs.com/cx0109/p/15491934.html)
{/* <body>
// 标签中data-xxx  xxx不要有大写字母在浏览器中显示会转化成小写  可以写成data-xxx-xx
   <div  data-uname="xiaoxiao" data-zhuiMeng="追梦" data-je-ffrey="jeffrey">777777777</div>
</body>

<script>
const divDom=document.querySelector("div")
//给dataset中的属性赋值和取值只有下面两种正确的写法方式
divDom.dataset.sex="女" //正确赋值写法
divDom.dataset.setCss = "设置css样式";//正确赋值写法 html中会显示成data-set-css
console.log(divDom.dataset.sex,divDom.dataset.setCss) //正确取值写法
// divDom.dataset['index-qw'] = "asd"; //错误写法
console.dir(divDom) //打印divDom的属性和方法
console.log(divDom,divDom.dataset,divDom.dataset.setCss,divDom.dataset.sex);
</script> */
}

//vue2中在template中使用import导入的变量属性和方法会报错(vue3中可以使用)，如果想要在模板中使用需要把变量属性赋值给data中的属性，把方法赋值给methods中的属性(实际时把方法在methods中再写一遍)


//小兔先uniapp项目注意事项
// 小程序是多页面应用所以不需要scoped，所以小程序开发不需要写scoped  小程序打包到h5端和app端，h5端(手机版网页)和app端是单页面应用所以需要scoped，所以会默认开启scoped

// index.vue(首页),PageSkeleton.vue(首页骨架屏),goods.vue(商品详情),profile.vue(个人信息),cart.vue和cart2.vue和CartMain.vue(购物车页面),address-form.vue(新增或修改地址)  这几个页面有兼容性的处理可以好好学习下

// uniapp(vue3)所需安装的插件和配置(https://www.bilibili.com/video/BV1Bp4y1379L?p=6&vd_source=009ec7b09b6a18588257bad29cd0d0cc)
// 安装插件
// 1.uni-create-view //快速创建uni-app页面
// 2.uni-helper //uni-app 代码提示
// 3.uni-app小程序扩展 //鼠标悬停查文档

// element ui 表单校验

// :rules="[
//   { required: true, message: '请输入邮箱地址', trigger: 'blur' },
//   { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }  //type常用校验的值有 'number','email','url'
// ]"


//在vue开发中为什么选择了 css-module 而不是 scoped?(https://juejin.cn/post/7254792532650836005)

// vue3中Suspense中使用
{/* <Suspense>
    <template #default>
    // div这个根标签可能是必须的，是不是必须的实际项目可以试下，不是很确定
      <div> 
      aaaaa
      </div>
    </template>
    <template #fallback>
      <span>Loading...</span>
    </template>
  </Suspense> */}

  // vue项目中，实现拖拽动态改变元素宽高(https://blog.csdn.net/weixin_41923266/article/details/126594753)

  //解决滚动条加载出现的页面抖动(https://www.cnblogs.com/Zxq-zn/p/12241070.html)
  // body {
  //   width: 100vw;
  //   overflow: hidden;
  //   padding-left: calc(100vw - 100%);
  // }

  // JS 点击某元素(class选择器是ssdw)以外的地方触发事件(https://blog.csdn.net/weixin_45606890/article/details/116748760这个链接作为参考)
  document.body.addEventListener("click", (e)=>{
    //e有可能为undefined  ssdw选择器中的子元素是element ui中的组件 组件中有点击事件触发时，这里的e为undefined
    if (e) {
        if (!e.target.closest(".ssdw")) {
          //写逻辑
        }
      }
  });


  // 前端百万级数据导出 Excel 方案(https://www.jianshu.com/p/aa5fb13e92e5)
  //难点打包体积的优化，支持按需引用这些(github搜billd-ui) 不知道行不行可以试下
  //浏览器ctrl+p可以搜索项目中的某个xxx.vue文件

function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
        for (var j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                var temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
}

// 示例
var arr = [64, 34, 25, 12, 22, 11, 90];
console.log("排序前：" + arr);
console.log("排序后：" + bubbleSort(arr));

//校验：只能输入特殊字符和汉字
// /[!@#$%^&*()（）_+\-—=\[\]【】{};| ':"\\,.<>\/?~`·！￥……”：；‘，。、《》？“’\u4e00-\u9fa5]+/g 


//js offsetTop和offsetParent offsetTop是一个元素的属性，用于获取该元素相对于其offsetParent顶部内边距边界的垂直偏移量,
//offsetTop属性返回的是一个元素顶部边界相对于其 offsetParent 元素顶部边界的距离。offsetParent 元素是最近的已定位（positioned）祖先元素 



//文件下载(https://blog.csdn.net/weixin_50799082/article/details/128547482)

//axios中使用AbortController取消请求(https://www.51cto.com/article/786053.html,https://www.jianshu.com/p/79919aa6cf5a)

//前端二进制一次性搞清楚(https://juejin.cn/post/7046313942938812424/)



//js如何控制一次只加载一张图片，加载完成后再加载下一张
const imgArrs = [...]; // 图片地址
const content = document.getElementById('content');
const loadImg = () => {
  if (!imgArrs.length)  return;
  const img = new Image(); // 新建一个Image对象
  img.src = imgArrs[0];
  img.setAttribute('class', 'img-item');
  img.onload = () => { // 监听onload事件
    // setTimeout(() => { // 使用setTimeout可以更清晰的看清实现效果
      content.appendChild(img);
        imgArrs.shift();
        loadImg();
    // }, 1000);
  }
  img.onerror = () => {
    // do something here
  }
}
loadImg();


//12个可能你没见过，但非常实用的 HTML 标签(在微信中搜索这个)

//vue2中created、mounted、watch、computed的执行顺序(https://juejin.cn/post/7109844650764009479?searchId=20240522093232793633803B3BD84709A3)
immediate 为false时： created =>beforeMount=> computed => mounted => watch   // beforeMount可以不关注，因为实际这个生命周期中不会写业务代码
immediate 为true时： watch =>created=>beforeMount=> computed => mounted  // beforeMount可以不关注，因为实际这个生命周期中不会写业务代码

//axios中接口防重(https://github.com/GuJiugc/JueJinDemo)

//vue项目中标签页关闭和刷新都不会走onBeforeUnmount和onUnmounted钩子函数
//浏览器标签页刷新的时候会走beforeunload  unload  onload这3个事件但是unload(现代浏览器被废弃)  onload通常用vue的mounted替代
//浏览器标签页关闭时推荐使用beforeunload，不推荐使用unload(现代浏览器被废弃)


// webpack打包优化
// 1.安装 image-webpack-loader来压缩图片，小于 10kb 的图片转化为 base64   limit: 1024 * 10
// 2.使用CommonsChunkPlugin配置将第三方库(node_modules)内容打包进vendor.js这个文件中，由于第三方库中的代码不会改动


// 接口的status报faild:ERR_UNESCAPED_CHARACTERS这个错误意思是请求路径包含未转义的字符 由于url中包含特殊字符（如中文字符或空格）或非英文数字造成的（http://www.imangodoc.com/4Ize1P30.html）
// 解决办法使用encodeURIComponent对url中传递的有特殊字符参数包裹


//在vue3中使用匿名函数自执行
(async ()=>{
  const res=await fetch("xxxx")
})()


//前端同源和不同源通信(https://www.bilibili.com/video/BV16V4y1w7uu/?spm_id_from=333.337.search-card.all.click&vd_source=009ec7b09b6a18588257bad29cd0d0cc)
js 不同源之间的通信
//发送页面
const href = "xxxx";
let otherWindow = window.open(href);
const url = new URL(href);
otherWindow.postMessage('Hello, Page 2!', url.origin);

//接收页面
window.addEventListener('message', function(event) {
  const origin='http://example.com'  //这个时发送页面的origin
  // 检查来源是否是预期的源
  if (event.origin !== origin) {  //这个判断写不写都行，严谨一点可以写，不写也可以，写的话需要在组件中通过组件路由拿到发送页面的origin
    return;
  }
  console.log('Received message:', event.data);
});


// 钉钉直接扫码进入小程序中的某个页面（把下面链接放入草料二维码中生成二维码，扫这个二维码直接进入页面，比打开小程序再找到这个页面更快更方便,在钉钉小程序文档应用开发搜索scheme
// 下面page 参数里的这一堆内容是由 encodeURIComponent(`pages/index/index?x=${encodeURIComponent('中文')}`);生成的）

dingtalk://dingtalkclient/action/open_mini_app?miniAppId=5000000005153628&page=pages%2FNewRegistration%2FNewRegistration           登记页面

dingtalk://dingtalkclient/action/open_mini_app?miniAppId=5000000005153628&page=pages%2FTransitApplication%2FTransitApplication      转运页面

dingtalk://dingtalkclient/action/open_mini_app?miniAppId=5000000005153628&page=pages%2Findex%2Findex    首页


{
  //try catch finally中的return执行顺序
  function sff(){
    try{
      console.log(44);
      return 5
    }finally{
      console.log(11111);
      // return 66
    }
  }
  console.log(sff());
  
}

//推荐使用下面的文档注释
/**
 * 把歌词解析成对象
 * 
 */
function parse(){

}
parse()


//transition只是针对数值类的属性有效，比如，width,height,opctity等等   而block=>none 属性变化不起效果
//在操作DOM元素的transform属性时，直接改变transform的值，如缩放scale、旋转rotate、平移translate等，通常不会直接触发页面的布局重排（reflow）
// 修改一个元素的 transform属性之所以不会直接影响父元素或相邻元素的布局，是因为 transform 是一种视觉变换效果，它发生在CSS渲染层面上，不影响元素在HTML文档流中的布局位置和尺寸
//修改一个元素的 transform属性只会发生在gpu硬件draw阶段(最后的阶段)
// vue3中addEventListener的妙用,当我们点击某块区域的时候，需要先判断是否在指定的区域内，然后再执行相应的逻辑(https://juejin.cn/post/7269411820905316389)


//Object.freeze冻结不会冻结访问器(get和set)
  {
    const qq={name:'www'}
    let eee=0
    Object.defineProperty(qq,"age",{
        get(){
            return eee
        },
        set(val){
          eee=val
        }
    })
    Object.freeze(qq)
    qq.age=200
  }

  {
    //let const 作用域保证每次循环都能打印出当前循环的值
    const obj=[1,2,3,4,5]
    for(const [index,value] of Object.entries(obj)){
      setTimeout(()=>{
        setTimeout(()=>{
          console.log(index,value);
        })
      })
    }
  }
   
  //常用time和timeEnd测试一段代码执行需要多少时间
 console.time("loop")
  const start=Date.now()
  while(Date.now()-start<2000){

  }
  console.timeEnd("loop")

  //js中原型链条就是隐式原型组成的链条

  //微信公众号
  //45 个每个开发人员都应该知道的 JavaScript 超级技巧
  //30+ 个工作中常用到的前端小知识（干货）
  //25 个杀手级 JavaScript 单行代码让你看起来像个专业人士
  //25 个JavaScript单行代码让你看起来像个专业人士
  //非常有用的 48 个 JavaScript 代码片段
  // 使用现代JavaScript操作DOM的高效模式
  //Vue3实现excel文件预览和打印
  //飞书一键复制网页内容为图片原理
  //这几个开源微信小程序，太有想法了（附源码）
  //uniapp 开发神器，效率翻倍
  //前端最常用的25个正则表达式，代码效率提高 80%
   //想提升前端代码开发效率，选对 AI 真的很重要 用这个网址 https://metaso.cn/ 
  // 12个移动端常见问题解决方案
  //掌握这 20 个 Git 命令，成为团队协作高手
  //unibest ：最好的uniapp开发模版框架
  //前端接私活的神器！提供了1500+免费的网页模板
  //聊聊前端技术调研到底应该怎么做？
  //前端架构师应该如何成熟的思考项目重构
  //深度剖析：高级前端Vue开发的技术框架组合秘籍
  //太酷了！Vue3 3D开发新选择！
  //20 个让人惊叹的 JavaScript 单行代码技巧，效率瞬间提升
  //Vue3 最佳实践：探秘 Douyin-Vue 仿抖音移动端
  //你还不知道的大文件上传
  //http://vue.easydo.work/这个网站可以啊 一堆后台模板
  //AI 联动！现在前端都流行这样开发~ 效率提升 100%
  //Vite打包优化实践：从分包到性能提升
  //“震惊！这款国产神器让前端开发效率提升5倍，产品经理都能自己写代码了？！”(uni-app低代码开发工具)
  //VTJ 一款基于 Vue3 + Typescript 的低代码页面可视化设计器(内置了element-plus和and design vue组件库,推荐使用)
  // 别再用 100vh 了！移动端视口高度的终极解决方案
  // Vue3 开发新选择：又一 Hooks 神库开源！(受够了手写请求防抖 / 缓存 / 重试逻辑？试试这个库吧！)
  //踩坑无数后，我终于总结出这份最全的 Vue3 组件通信实战指南
  // Vue3 新趋势：请不要再用 Transition！最强动画库诞生(github 62.3k star) 还有https://motion.vueuse.org/和,https://vue-bits.dev/text-animations/ascii-text
  // Vue3 新趋势：一键最强监控方案 来啦！
  // vue3 生态中，真正对标React 生态 UMI 的 企业级 框架！
  // 尤雨溪力荐 Vue-Plugins-Collection！Vue 生态 最强插件 导航！
  // UniApp集成Apple Pay支付完整指南
  // 比 html2canvas 快 30 倍的截图神器！项目已使用！
  // 做 DOCX 预览？这个神库别错过！
  // JSON.stringify() 的进阶用法：解锁你不曾了解的强大功能
  // ElementPlusX + RuoyiAI：Vue3 首个 AI 开发模板开源了！
  // 告别重复导入！Vite + unplugin-auto-import 让 Vue 开发效率翻倍
  // 我用 54000 分钟打造的后台系统模板，开源了
  // 周下载量百万级的前端截图工具库 html-to-image 轻松实现 JS 截屏
  // Vue3 + XGPlayer 打造专业级视频播放器 - 功能丰富性能优秀（西瓜视频开源的 HTML5 视频播放器） 
  // Vue 项目必备！轻松实现 uniapp/H5/PC 复制功能
  // 90% 前端都不知道的 20 个「零依赖」浏览器原生能力！
  // 10 个 JS 神 API？复制即用，今晚早下班
  // 一行代码生成绝对唯一 ID？别再用 Date.now() 了 ！(crypto.randomUUID())
  // 在页面关闭时，前端上传监控数据的4个解决方案
  // 调试利器！VSCode 瞬间化身 “Chrome遥控器”！(线上用户反馈页面白屏，你却本地复现不了)
  // 前端代码更新，如何优雅地通知用户刷新页面？
  // 一行代码解决图片懒加载，图片懒加载的终极简化！
  // 前后端防重复提交的 6 种落地实现：从按钮禁用到 AOP 全自动防护
  // 10年老前端，分享20+严选技术栈
  // 90% 的前端都没摸过的 20 个 JS 神级 API！
  //投简历要一个月的月中投，不要在月初(1-5)和月末(25-月底)投，在周2或3或4投，尤其时周2，周一和周五不投简历，投简历要在上午9-11点之间投，下午2-4点之间投，
  // 优先关注一些发布时间不到72小时的岗位，如果这个岗位标注紧急岗就更好了，更容易入职，如果发布时间超半年的岗位就不要投了，可能是钓鱼岗位，根本不缺人。要么是要求太高，竞争过于激烈
  // https://github.com/Daymychen/art-design-pro  后台管理系统模板
  // 分享大厂使用最多的 7 个高效开发工具库
  // 几种虚拟列表技术方案调研
  // 大文件预览选择Blob URL的原因
  // Vue3 手势指令发布！(vue3-touch-events 来了！这是一个专为 Vue3 设计的强大手势事件库，让你轻松实现各种触摸手势交互，完美支持移动端和桌面端)
  // 独立开发者如何养活自己？
  // 99% 的前端工程师，都被这里的算法“教育”过(它用 JavaScript，硬生生把所有主流算法重新写了一遍)
  //微信公众号

  //前端面试
  https://interview.poetries.top/
  https://vue3js.cn/
  https://www.yuque.com/cuggz
  //微信公众号
  

  //基于element ui中el-tree扩展实现tree树虚拟滚动功能（https://blog.csdn.net/Brad_chao/article/details/121082281）
  // vue-easy-tree  github地址（https://gitcode.com/gh_mirrors/vu/vue-easy-tree/overview?utm_source=csdn_github_accelerator&isLogin=1）


  {
    //功能：滚动条滚动到指定元素位置(也可以使用scrollIntoView,但是如果多个元素都scrollIntoView可能会相互影响，相互影响就用下面这种方式)
    //element是当前dom,scrollParent是具有滚动条的祖先dom同时祖先元素的样式加上scroll-behavior: smooth;
    const element=document.querySelector(".current")
    const scrollParent=document.querySelector(".ancestors")
    function getOffsetTopToScrollParent(element, scrollParent) {
      let offsetTop = element.offsetTop;
      let offsetLeft = element.offsetLeft;
    
      let currentElement = element.offsetParent;
    
      while (currentElement && currentElement !== scrollParent) {
        offsetTop += currentElement.offsetTop;
        offsetLeft += currentElement.offsetLeft;
        currentElement = currentElement.offsetParent;
      }
    
      return { top: offsetTop, left: offsetLeft };
    }
  
    const { top,left } = getOffsetTopToScrollParent(element, scrollParent);
    //横向滚动
    scrollParent.scrollLeft = left;
    //纵向滚动
    scrollParent.scrollTop = top;
    
  }


 


  //在 Vue Router4 中 router.match 改为 router.resolve 的使用场景
  1. 动态路由匹配
  当你需要根据用户输入或某些条件动态生成路径时，可以使用 router.match 来验证该路径是否有效。例如，在一个搜索框中，用户输入的 URL 可能需要验证是否存在对应的路由
  const userInput = '/users/123';
  const matchedRoute = router.resolve(userInput);

  if (matchedRoute) {
    console.log('Valid route:', matchedRoute);
  } else {
    console.log('Invalid route');
  }
  2. 生成面包屑导航
  在一些应用中，面包屑导航需要根据当前路径动态生成。可以使用 router.match 来获取当前路径的路由信息，并根据这些信息构建面包屑
  const breadcrumbs = [];
  const pathSegments = currentPath.split('/');

  pathSegments.forEach((segment) => {
    const matched = router.resolve(segment);
    if (matched) {
      breadcrumbs.push({ name: matched.name, path: matched.path });
    }
  });
  3.跳转到路由是home的新tab页
  const {href}=router.resolve("/home")
  window.open(href)


  //git+VScode 版本回退(https://blog.csdn.net/vv110x/article/details/131290157)



  vue3 退出登录重置pinia和pinia持久化储存
  {
    //store/index.js(针对组合式pinia，实现自定义$patch插件以及封装了重置pinia所有状态方法)
    import { getActivePinia,createPinia } from 'pinia';
    const pinia = createPinia()
    // 因为状态管理使用的是setup的方式(组合式写法)构建所以我们重写一个$reset重置state并挂载到pinia中 不写这个会报错，options写法创建的store也起作用
    pinia.use(({ store }) => {
      const initialState = JSON.parse(JSON.stringify(store.$state));
      store.$reset = () => {
        store.$patch(initialState);
      }
    })
    // 重置pinia
    export const resetPinia = () => {
      const getActive= getActivePinia();
      const stores = getActive._s; // 获取所有 storen
      stores.forEach((store) => {
        try{
          store.$reset(); // 重置每个 store 的状态
        }catch(err){
          console.log(err)
        }
      });
    }
    export default pinia
    //store/index.js


    //持久化储存  安装npm install pinia pinia-plugin-persistedstate
    //在main.js中
    import { createApp } from 'vue'
    import pinia from '@/stores'
    import App from './App.vue'
    import router from './router'
    import { createPersistedState } from 'pinia-plugin-persistedstate';//加这行
    const app = createApp(App)
    //auto: true，表示全部store都localStorage持久化，如果单个store中有persist: {storage: sessionStorage}，那么这个store就会使用sessionStorage持久化，
    // 如果有多个变量(a,b)都想使用sessionStorage持久化，其他的使用localStorage持久化，那么这些变量(a,b)必须都写在一个store中(牢记，不能分开写在多个store中)
    pinia.use(createPersistedState({ auto: true }));
    app.use(pinia)
    app.use(router)
    app.mount('#app')
    //在main.js中

    //store/counter.js  pinia中组合式写法
    import { ref, computed } from 'vue'
    import { defineStore } from 'pinia'
    export const useCounterStore = defineStore('counter', () => {
      const count = ref(0)
      const doubleCount = computed(() => count.value * 2)
      function increment() {
        count.value++
      }

      return { count, doubleCount, increment }
    },{
         // uniapp小程序端配置
        // persist: {
        //   storage: {
        //     getItem(key) {
        //       return uni.getStorageSync(key)
        //     },
        //     setItem(key, value) {
        //       uni.setStorageSync(key, value)
        //     },
        //   },
        // },
        //pc段端设置
        persist: {storage: sessionStorage}
      }
    )
    //store/counter.js
  }
 vue-router中在配置别名的场景下，采用path跳转，其他情况使用name跳转

 vue-route中守卫的to,from和组件中的$route对象的属性是一样的如下
 {
  fullPath :  "/"
  hash  :  ""
  matched: []
  meta  :  {}
  name  :  null
  params :   {}
  path :   "/"
  query  :  {}
 }

vue-router中路由跳转$router.push(xxx)中push的参数是一个对象，对象中的属性有path,name,params，query，hash,replace例如下面
this.$router.push({
  path:"/about",
  name: 'about',
  params: { id: 123 },
  query: { page: 1, sort: 'desc' },
  hash: '#section1',
  replace: false,1
});
实际使用场景不需要传这么多属性，这里只是展示一下总共可以传多少属性




pinia使用时注意事项
1.必须在state中定义的属性才能在组件中使用，虽然不在state中定义，在组件中使用也是响应式的，但是没有定义的属性在持久化缓存和重置state，调用store.$reset()的时候是不起作用的

2.组合式 store 是可以相互使用，Pinia 当然也支持它。但有一个规则需要遵循：
在 Pinia 中，你可以在一个 Store 的 computed 或 action 中读取另一个 Store 的 state，而不会违反 Pinia 的规则。
这是因为 computed 和 action 是在 Store 初始化之后才执行的，因此不会导致无限循环或直接互相读取对方的 state 的问题


//vue3响应式数据的本质(很重要)
1.被监控的函数：
1.1 render函数
1.2 computed传入的函数
1.3 watchEffect传入的函数
1.4 watch传入的第一个参数一般是函数(如果不是函数源码内部会转成函数)
2.被监控的函数运行期间(切记是运行期间)读取响应式数据(用ref或reactive创建的数据，响应式数据只能是对象)的某个属性(例如const age=ref(0),age.value)
3.响应式数据的这个属性改变时被监控的函数重新执行




在Vue3的模板语法中，表达式是指能返回值的单行JavaScript代码片段。具体包括：

基本运算：如{{ number + 1 }}、{{ 5+5 }}等算术运算
三元表达式：如{{ ok ? 'YES' : 'NO' }}的条件判断
方法调用与链式操作：如{{ message.split('').reverse().join('') }}的字符串处理
属性绑定中的拼接：如v-bind:id="'list-' + id"的字符串拼接
布尔运算与逻辑判断：如{{ isActive && show }}的布尔逻辑
不支持的情况包括：

语句（如var a = 1）
流程控制（如if/for语句）
多行代码或分号隔开的表达式（如let a=1; a+1）
副作用操作（如变量赋值counter++



//函数声明在严格模式下仅在块内有效，在非严格模式下函数声明在块外有效
//函数声明在非严格模式下会提升到块外，严格模式下不会提升到块外
"use strict";
{
  function foo() {
    console.log("块内函数");
  }
}
foo();//非严格模式打印块内函数，严格模式报错


{
// 在 Vue3 中，使用 :class 绑定数组时，数组中的元素包括字符串、响应式变量,对象、数组、计算属性、三元表达式等
// const baseClass = ref("base-class");
//const class2 = ref({ 'border': true });
// <div :class="[
//   'static-class',//字符串
//   baseClass,//响应式变量
//   class2,//响应式变量
//   isActive && 'active',  // 短路写法（相当于 isActive ? 'active' : false）
//   { 'error': hasError },//对象
//   computedClass,//计算属性
//   ['nested-array']//数组，数组可以嵌套一层，在多层就不推荐了
// ]"></div>

// 与静态 class 共存：
// <div class="static" :class="['dynamic']"></div>渲染结果：<div class="static dynamic"></div>

// 无效的元素类型:非字符串/对象/数组的值（如 null、undefined、数字、布尔值等）
// <div :class="[123, false, null, undefined]"></div>渲染结果：<div class></div>，即无效的元素类型不会被渲染


//Vue3 中，使用 :style可以绑定对象和数组，绑定数组时,数组中的每个元素可以是样式对象或返回样式对象的表达式

}


{
  在CSS中,样式是否会覆盖,取决于CSS规则的书写顺序和选择器的优先级，而不是HTML中class属性的顺序
}

//事件修饰符当使用系统按键修饰符+其他键时(例如 alt+a)，必须同时按住alt+a事件才会触发，不使用系统按键修饰符而是用其他键+其他键时(例如 a+b)，只要按下a键或b键其中一个键事件就会触发

//vue3中使用v-mode的值可以是字符串，数字，布尔值，数组，对象。自定义组件中v-mode的值可以是任意类型
//select标签多选框多选的时候需要按住ctrl键，这是浏览器的默认行为， 如果按住ctrl键觉得麻烦，可以使用elemet ui等ui库的多选(ui库中多选是自定义实现的)


//Vue3 <input v-model.number="age" /> <div>{{ age + 100 }}</div>
Vue3的.number修饰符会尝试将输入内容转换为有效数值类型，其底层实际调用的是parseFloat()函数（而非parseInt()或Number()）。这一行为会导致以下差异：
1.以数字开头的字符串（如"0aa"）：parseFloat("0aa")会提取开头的数字部分0，忽略后续非数字字符，此时age被赋值为数值0，0 + 100自然得到100
2.以非数字开头的字符串（如"aa0"）：parseFloat("aa0")无法解析出有效数字，直接返回原始字符串"aa0"，字符串与数值相加时触发隐式转换，"aa0" + 100等价于字符串拼接"aa0100"。

//对于<input>元素，change事件是在值改变且失去焦点后触发的

//vue3中事件校验和对 props 添加类型校验的方式类似，主要用于开发和调试阶段，但并不会影响实际的功能运行。


{
  vue2中props中的数据，赋值给data中的属性可以按下面方式写:
  export default { 
    props:[value],
    data(){
      return {
        content:this.value
      }
    }
   }
}


 {
  //JavaScript 赋值操作符 = 不仅会将右侧的值赋给左侧的变量，同时该赋值表达式本身也会返回右侧的值 ，这个是js语言的特性,下面的例子就说明了这一点：
  let sw=10
  let qas=20
  function ssw(){
    
    return qas=sw=20
  }
  console.log(ssw(),sw,qas)//20;

  let a,b;
  console.log(a= b = 55,a,b); // 55
  
 }


{
  当一个 prop 被声明为允许多种类型时，Boolean 的转换规则也将被应用。
  然而，当同时允许 String 和 Boolean 时，
  有一种边缘情况——只有当 Boolean 出现在 String 之前时，Boolean 转换规则才适用
  // 当父组件只写 disabled时，子组件的 disabled 值为 ""，如果想要disabled的值为true，父组件需要写成 :disabled="true"，
  <Child disabled></Child>
  defineProps({
    disabled: [String, Boolean]  //只有String在Boolean之前时，才会出现这种情况
    // disabled: [Boolean, String]  //这样就不会出现这种情况了
  })
  // 当父组件只写 disabled时，子组件的 disabled 值为 ""，如果想要disabled的值为true，父组件需要写成 :disabled="true"，


  //为了防止disabled为”“的情况，建议使用的写法disabled: [Boolean, String]
}


{
<input @input="text = event.target.value"> 和<Child @update:modelValue="foo = title">在 Vue3 的模板中，不能直接将 $event 替换为 event 或 title。以下是详细解释：

1. $event 是 Vue 的保留变量
$event 是 Vue 模板中的特殊变量，它指向事件触发时传递的值（无论是原生事件还是自定义事件）。
在模板的事件绑定表达式中，$event 是唯一合法的变量名，直接替换为其他名称（如 event 或 title）会导致错误。
❌ 错误写法：
<!-- 原生事件 -->
<input @input="text = event.target.value"> 

<!-- 自定义事件 -->
<Child @update:modelValue="foo = title">

✅ 正确写法：
<input @input="text = $event.target.value"> 
<Child @update:modelValue="foo = $event">


2. 为什么不能替换为 event 或 title？
event 和 title 在模板上下文中未定义：
Vue 的模板表达式在编译时会被转换为一个函数，而 $event 是这个函数的参数名。例如：

// 编译后的代码
function ($event) {
  text = $event.target.value
}
如果替换为 event 或 title，相当于引用了未声明的变量，会导致运行时错误。

$event 是 Vue 的硬编码关键字：
Vue 的模板编译器会直接依赖 $event 这个变量名来传递事件参数，开发者无法自定义。

3. 如何间接实现“自定义参数名”？
如果希望使用其他变量名（如 title），可以通过以下两种方式实现：

方法 1：在方法中接收并重命名参数
<Child @update:modelValue="handleUpdate($event)"/>
// 在组件中定义方法
methods: {
  handleUpdate(title) {
    this.foo = title; // 将 $event 重命名为 title
  }
}
方法 2：使用箭头函数（仅适用于简单逻辑）
<Child @update:modelValue="(title) => foo = title"/>
通过显式定义箭头函数参数，将 $event 重命名为 title


4. 原生事件 vs 自定义事件的 $event
原生事件（如 @input）：
$event 是原生的 Event 对象，需要通过 $event.target.value 获取值。
<input @input="text = $event.target.value">

自定义事件（如 @update:modelValue）：
$event 是子组件通过 emit 传递的值，直接使用即可。

<Child @update:modelValue="foo = $event"/>
在自定义事件中如果子组件传递多个参数$event只代表第一个参数，就不能使用foo = $event
传递多个参数父组件接收推荐这样写  
<Child @update:modelValue="handleFN"/>
function handleFN(...args){
  console.log(args)
}
}



{
  <template>
  <div>
     //当title是一个字符串，直接使用 title++ 会导致 NaN 错误
    <button @click="title++">{{ title }}</button>
  </div>
 
</template>
<script setup>
import { ref } from "vue";
const title = ref("hellow"); 

const handleswws = () => {
  title.value = "hello world";
};
</script>
<style>
</style>

}



{
  //在vue2中使用provide
  export default { 
    // provide:{age:10},
    // 如果想要使用data中的数据比如content需要这样写,如果想要age是响应式数据把content换成obj就可以了
    // provide(){
    //   return {age:this.content}
    // },
    //如果想要传递响应式数据
    data(){
      return {
        content:"sssw",
        obj:{age:10}

      }
    }
   }
   //在vue3中使用provide
   import { ref, provide,inject } from 'vue'
   <script setup>
    //父组件中使用provide
    const title = ref("我是爷爷组件");
    function updatetitle(val) {
      title.value = val;
    }
    provide("title", {
      title: readonly(title), //readonly是只读的意思，readonly和ref结合使用，readonly是为了让子组件不能修改父组件传过来的值
      updatetitle,
    });provide和readonly一般结合使用，让子孙组件不能修改传过来的值
   </script>

  <script setup>
  //子组件中使用inject
  const titleObj = inject("title", "默认值"); //第二个参数是默认值，没有提供provide就会使用默认值
  setTimeout(() => {
    titleObj.updatetitle("我要修改传递过来的title值"); //可以在子孙组件中修改父组件传递过来的值
  }, 3000);

const handleTitle = () => {
  titleObj.title.value = "我是孙子组件传过来的"; //会报警告因为title是一个readonly只读的
};
  </script>
}



{
  <transition>
    <span :key="text">{{ text }}</span>
  </transition>
  //当 text 的值发生变化时,Vue 会销毁旧的 <span> 元素并创建一个新的 <span> 元素来显示新的 text 值，span元素换成组件也是一样的道理
}


{
  //一个父组件中有个子组件，父子组件生命周期钩子函数挂载的执行顺序是：
  //父组件的beforeCreate -> 父组件的created -> 父组件的beforeMount -> 子组件的beforeCreate -> 子组件的created -> 子组件的beforeMount -> 子组件的mounted -> 父组件的mounted

  //在Vue中，如果修改了响应式数据但该数据没有在模板中使用，更新的生命周期钩子（如beforeUpdate和updated）将不会执行
}


{
  在 Vue 中， 兄弟组件（Sibling Components） 是指具有相同父组件的多个子组件。换句话说，如果两个组件在同一个父组件中被引入和使用，它们就是兄弟组件
  兄弟组件 = 同父组件的多个子组件
  //兄弟组件通信：
  // src/utils/bus.js 
  import mitt from 'mitt'; 
  export default mitt(); 

  发送方 (ComponentA)
  <template> 
    <button @click="sendMessage">发送消息</button> 
  </template> 

  <script setup> 
    import bus from '@/utils/bus'; 

    const sendMessage = () => { 
        bus.emit('message', { text: 'Hello from A!' }); 
    }; 
  </script> 

  接收方 (ComponentB)
  <template> 
    <p>{{ receivedMessage }}</p> 
  </template> 

  <script setup> 
    import { ref, onMounted, onUnmounted } from 'vue'; 
    import bus from '@/utils/bus'; 

    const receivedMessage = ref(''); 

    const handleMessage = (data) => { 
        receivedMessage.value = data.text; 
    }; 

    onMounted(() => { 
        bus.on('message', handleMessage); 
    }); 

    onBeforeUnmount(() => { //这里推荐用onBeforeUnmount 解绑事件监听器也是用onBeforeUnmount，不推荐用onUnmounted，onUnmounted可能会有边界问题。
        bus.off('message', handleMessage); // !!!必须手动解绑防止内存泄漏 
    }); 
  </script> 
}



{
  //使用AbortController取消上次请求(相当于防抖功能，在vue3中比较常用)
  watchEffect(async (onCleanup) => { 
    const controller = new AbortController(); 
    // 🔧注册请求取消逻辑 
    onCleanup(() => controller.abort()); 

    try { 
        const res = await fetch('/api/data', { 
            signal: controller.signal 
        }); 
        console.log(await res.json()); 
    } catch (err) { 
        if (err.name == 'AbortError') {
           //这个错误是咱们自己主动取消请求造成的
        }else{
          //真正的网络请求错误
          console.error(err);
        }
    } 
});
}


{
  //vue3中插槽还可以这样用<component> 完全支持渲染来自插槽的 VNode(不知道使用这种方式的优势在那，这种用法应该不常用，了解下就可以了)，
  //父组件
  <template>
  father
  <button @click="handle">father按钮</button>
  <child ref="childRef">
    <span>father1</span>
    <span>father2</span>
  </child>
  </template>
  <script setup>
  import child from "./child.vue";
  import { reactive, ref, toRefs, useAttrs, watchEffect, watch } from "vue";
  const childRef = ref(null);
  const title = ref("父组件");
  const handle = () => {
    console.log(childRef, "父组件点击");
  };

  defineExpose({
    title,
    handle,
  });
  </script>
  //子组件 不用<slot /> 直接使用component渲染插槽内容，<component :is="$slots.default()[0]" />渲染结果是father1，<component :is="$slots.default()[1]" />渲染结果是father2
  // $slots.default()[0]和slotss.default()[0]效果是一样的，直白点说就是模板中的$slots和script中的useSlots()是一样的
  <template>
  <component :is="$slots.default()[0]" /> 
  <component :is="$slots.default()[1]" />
  <component :is="slotss.default()[0]" />
  <component :is="slotss.default()[1]" />
  <button @click="hansssdle($parent)">child按钮</button>
  </template>
  <script setup>
  import { ref, useSlots } from "vue";
  const name = ref("子组件");
  const slotss = useSlots();
  console.log(slotss.default(), "子组件的插槽");

  const hansssdle = (parent) => {
    console.log(parent, "子组件点击");
  };
  defineExpose({
    name,
    hansssdle,
  });
  </script>
}


{
  Vue3中的setup函数相当于Vue2中的created生命周期钩子。根据证据，setup函数在Vue3中取代了Vue2中的beforeCreate和created生命周期钩子，
  通常在组件实例化后、模板渲染前执行，用于初始化组件的状态、数据和方法
}


{
  //获取一个对象中部分属性和属性值所组成的对象
  const obj = {
    name: '张三',
    age: 20,
    sex: '男',
    name1: '张三1',
    };
    //想要获取除了name1属性之外的所有属性和属性值所组成的对象
    const { name1, ...rest } = obj; // rest对象中包含了除name1之外的所有属性和属性值
    console.log(rest);
    
}

{
  //Vue3中watchEffect不会再次执行的示例(项目代码中要避免这种写法)
  //第一种
  <script setup>
  import { ref, watchEffect } from"vue"

  const a = ref(true)
  const b = ref(false)

  watchEffect(() => {
    // 这里的 a.value 和 b.value 都是响应式的
    // 如果 a.value 为 true，则不会执行 b.value 的计算
    // 如果 a.value 为 false，则会执行 b.value 的计算
  if (a.value || b.value) { //由于a.value是true，所以b.value没有执行，所以修改b.value不会触发watchEffect重新执行
      console.log('执行了更新操作');
    }
  })

  const test = () => b.value = !b.value;
  </script>

  <template>
    <button @click="test">改变b的值</button>
  <h2>当前b的值:{{ b }}</h2>
  </template>
  //第二种
  <script setup>
  import { ref, watchEffect } from"vue"

  const getInfo = async () => {
  awaitnewPromise((resolve, reject) => {
      setTimeout(() => {
        resolve(111)
      }, 2000)
    })
  }

  watchEffect(async () => {
  // 请求信息
  await getInfo()
  if (b.value) console.log('执行了更新操作');//由于b.value是在异步之后执行，所以不会触发watchEffect重新执行
  })

  const test = () =>  b.value = !b.value;
  </script>

  <template>
    <button @click="test">改变b的值</button>
  <h2>当前b的值:{{ b }}</h2>
  </template>
}

{
  //vue3组合式写法（script setup）中内容的先后顺序
  <script setup>
  // import语句
  // Props(defineProps) 
  // Emits(defineEmits) 
  // 响应性变量定义 
  // Computed 
  // Watchers 
  // 函数 
  // 生命周期 
  // Expose(defineExpose)
  </script>
}


{
  //pinia中storeToRefs只能解构一层
  <script setup>
    pinia中show.js
const useushowStore = defineStore('show', () => {
    const show = reactive({count:0})
    return { show}
  }
)
import { useushowStore } from "@/stores/show";
import { storeToRefs } from "pinia";
import { toRefs } from "vue";
const { show } = storeToRefs(showStore); // 解构show（此时show是Ref）
const { count } = toRefs(show.value);// 这里toRefs不能换成storeToRefs,否则报错,因为show.value对象的属性值不是ref或reactive对象，所以不能使用storeToRefs解构
console.log(count, "countsssss");
</script>

<template>
  <AboutView :age="age"></AboutView>
  <main>comsss</main>
  <button @click="showStore.show.count++">修改count</button>
  <div>{{ count }}-count</div>
</template>

<style>
/* 默认激活样式 */
.router-link-active {
  color: blue;
  font-weight: bold;
}

/* 完全匹配的激活样式 */
.router-link-exact-active {
  color: red;
}
</style>

}



{
  Vue中父子组件交互推荐使用 props 和 $emit 尽量不要使用$parent 和 this.$refs.xxx（牢记）
}


{
  //低代码引擎tiny-engine中导入的element-plus组件库的使用中菜单ElMenu中的子菜单不能单独使用，ElMenu的子菜单必须要有父菜单，步骤条Steps 的子菜单不能单独使用，必须要有父菜单，
  // 单独使用项目启动项目不报错，运行时会报错
  
}

{
  //elemet-plus中分页的使用，size-change和 current-change事件的使用不能使用了，使用以下方式
  <template>
  <el-pagination
    v-model:currentPage="currentPage"
    v-model:pageSize="pageSize"
    :page-sizes="[10, 20, 30, 40]"
    layout="total, sizes, prev, pager, next, jumper"
    :total="total">
  </el-pagination>
</template>

<script>
export default {
  data() {
    return {
      currentPage: 1,
      pageSize: 10,
      total: 100
    };
  },
  watch: {
    currentPage(newVal, oldVal) {
      // 当前页码发生变化时执行的业务逻辑
      console.log(`当前页码从 ${oldVal} 变为 ${newVal}`);
      this.fetchData(newVal, this.pageSize);
    },
    pageSize(newVal, oldVal) {
      // 每页显示条数发生变化时执行的业务逻辑
      console.log(`每页显示条数从 ${oldVal} 变为 ${newVal}`);
      this.fetchData(this.currentPage, newVal);
    }
  },
  methods: {
    fetchData(page, size) {
      // 模拟数据请求
      console.log(`请求数据，当前页码：${page}，每页显示条数：${size}`);
      // 这里可以写实际的数据请求逻辑
    }
  }
}
</script>

}



{
   //pc端开发环境代理注意事项：
   const service = axios.create({
      baseURL: process.env.VUE_APP_BASE_API, // 下面的/low-code这个值必须和这里的process.env.VUE_APP_BASE_API的值一样
      timeout // 请求超时
    })
   //vue2写法：
   proxy: {
      //  /low-code这个值必须和
      "/low-code": {
        // ws: true,  // 跨域使用
        //http://172.20.103.72:9793 高峰
        target: "http://172.20.236.11:9793/low-code", // java
        // target: "http://10.0.66.101:9793", //"http://10.0.66.102:9793",
        changeOrigin: true,
        pathRewrite: {
          "^/low-code": ""
        }
      },
    }
    vue3写法：
    server: {
      proxy: {
        "/low-code": {
          target: "http://172.20.236.11:9793/low-code",
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/low-code/, "")
        }
      }
    }
}



{
  function fetchUser(id) {
 return new Promise(resolve => {
    setTimeout(() => {
      resolve({ id: id, name: `User ${id}` });
    }, 1000); // 每个请求耗时 1 秒
  });
}

// 错误示范：在 for 循环中串行使用 await
async function fetchAllUsers(userIds) {
 console.time('Fetch All Users');
 const users = [];
 for (let id = 1; id <= userIds.length; id++) {
    // 关键点：循环会在这里暂停，等待上一个请求完成后再开始下一个
    console.log(id,"id")
    const user = await fetchUser(id);
    users.push(user);
    console.log(id,"id")
  }
 console.timeEnd('Fetch All Users');
 // 假设这里是更新 UI 的操作
//  renderUsers(users); 
 return users;
}

const userIds = [1, 2, 3, 4, 5];
fetchAllUsers(userIds); 

//for...of中的await为什么会是继发(上一个执行完才执行下一个)执行而不是并发(可以理解为同时)执行，因为for循环会等待循环体内的代码执行完毕后，进行下一次循环，因此它会等待内部的 await 结果，因此循环是继发执行的
//
}



{
  //Storage存储
//sessionStorage共享数据的前提是
//1.必须同源
//2.必须在同一个标签页中

//sessionStorage 的存储容量通常为 5MB 左右
//sessionStorage 的生命周期是浏览器标签页关闭或浏览器窗口关闭时被清除

//sessionStorage在项目中的使用场景
//A页面和B页面是同源的，A页面通过window.open()跳转到B页面，A和B是两个标签页，A页面中的sessionStorage值会被复制(切记这里是复制，不是共享)到B页面中，
// 然而，需要注意的是，尽管B页面继承了A页面的 sessionStorage 数据，但这两个页面的sessionStorage是独立的，即在A页面中修改sessionStorage不会影响B页面中的数据，反之亦然
//sessionStorage在项目中的使用场景

// localStorage 的生命周期是永久的，除非被手动清除或通过 JavaScript 代码删除，否则数据会一直保存在用户的浏览器中 ，
// 这意味着即使用户关闭浏览器或电脑重启，localStorage 中存储的数据仍然存在。如果需要清除 localStorage 中的数据，可以通过以下方法实现：

// 使用 localStorage.removeItem('key') 删除特定的键值对。
// 使用 localStorage.clear() 清除所有存储的数据。
// 需要注意的是，localStorage 的存储容量通常为 5MB
}


{
   //小程序开发为什么要选择在iPhone 6 模式下？
  //小程序开发中使用的是rpx，iPhone 6 模式的核心优势是‌简化单位换算‌（px 直接翻倍为 rpx），比如，在调试器中设置padding:20px，那么在实际代码中就写padding:40rpx，
  //由于在调试器中设置rpx是不起效果的，所以需要在调试器中使用px,然后在代码中乘以2转成rpx，调试时用 px 验证逻辑，最终代码用 rpx 确保多设备兼容性‌
  // 

}

{
  vue3z中flush: "post"的watchEffect 和 nextTick的执行顺序：
  在大多数情况下，watchEffect 和 nextTick 的回调函数会几乎同时执行，但在实际运行中，nextTick 的回调函数可能会在 watchEffect 的回调函数之后执行
}

{
  function ww(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
           resolve("reject")
        },3000)
    })
  }
 async function fs() {
    console.log(2)
    let res=await ww()
    console.log(3)
    return res
 }
 function wer(){
     let dd=fs()
     console.log(dd)
     console.log(1)
     setTimeout(()=>{
      console.log(dd)
     },4000)
 }
 wer()
 let qq=fs()
 qq

 function ww(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
           reject("出错了")
        },3000)
    })
  }
  async function fs() {
    let res=await ww()
    return res
 }
  fs()//在浏览器中结果是Promise.reject('出错了')，得出结论是只要async函数中await 后面是Promise.reject(xxx)，那么这个函数的结果就是Promise.reject(xxx)


  
}

{
  let ss=50
  function fetchUser() {
    console.log(ss)
   }
  function fetchData() {
    let ss=100
    fetchUser()
  }
  fetchData()
}

{
  
}